package application.snakes;
import java.util.LinkedList;

import enums.CellType;
import enums.Direction;

import application.Coordinate;
import application.GameManager;
import application.GameMatrix;
import application.auxclasses.MakeMoveParameter;
import application.food.*;
import application.visitorPattern.SnakeVisitor;


public abstract class Snake implements SnakeVisitor{

	public static final int INITSIZE = 5;
	private GameMatrix _matrix;
	private int _MatrixSize;
	private Direction _Direction;
	private LinkedList<Coordinate> _body;
	private boolean _isFuzzy;
	private int _score;
	private String _type;
	private GameManager _GameManager;
	/**
	 * constructor. Initial direction is east. Initializes the body from the 
	 * GameMatrix instance
	 * @param matrix
	 */
	public Snake(GameMatrix matrix,GameManager manager) {
		this._GameManager = manager;
		this._matrix = matrix;
		this._MatrixSize = matrix.get_dimension();
		this.set_direction(Direction.EAST);
		this.set_type("regular");
		this._isFuzzy = false;
		this._body = this._matrix.InitSnakeBody(Snake.INITSIZE);
		this.resetScore();
		this._matrix.print();
	}
	/**
	 * Infers the new head and tail coordinates depending on direction. Direction
	 * might change before move() is called 
	 */
	public synchronized void move(){
		Coordinate tTailOldCoord = this._body.getLast();
		Coordinate tHeadOldCoord = this._body.getFirst();
		Coordinate tHeadNewCoord = (Coordinate)tHeadOldCoord.clone();
		Coordinate tTailNewCoord = (Coordinate)tTailOldCoord.clone();
		
		MakeMoveParameter tMoveCoords = 
			new MakeMoveParameter(tHeadOldCoord,tHeadNewCoord,tTailOldCoord,
					tTailNewCoord);
		
		inferDirection(tMoveCoords);
		this.makeMove(tMoveCoords);
	}
	/**
	 * decides which coordinate the snake moves into on the next move
	 * @param tMoveCoords
	 */
	private void inferDirection(MakeMoveParameter tMoveCoords) {
		Direction d = this.get_direction();
		if(d == Direction.EAST){
			GameManager.print("going east");
			tMoveCoords._newHead.incX();
		}else if(d == Direction.SOUTH){
			GameManager.print("going south");
			tMoveCoords._newHead.incY();
		}else if(d == Direction.WEST){
			GameManager.print("going west");
			tMoveCoords._newHead.decX();
		}else if(d == Direction.NORTH){
			GameManager.print("going north");
			tMoveCoords._newHead.decY();
		}
	}
	/**
	 * makes the snake move depending on the new coordinates
	 * @param movecoords the new coordinates
	 */
	private void makeMove(MakeMoveParameter movecoords) {
		GameManager.print("checking from ("+
				movecoords._oldHead.get_x()+","+
				movecoords._oldHead.get_y()+") to "+
				movecoords._newHead.get_x()+","+
				movecoords._newHead.get_y()+")");
		int tMoveType = isLegalMove(movecoords._newHead);
		if(tMoveType > 0){
			if(tMoveType < 2){
				this.advanceHeadOnce(movecoords._oldHead,movecoords._newHead);
				noEatMove(movecoords);
			}else{
				Food f = this._matrix.getCell(movecoords._newHead).get_food();
				this.advanceHeadOnce(movecoords._oldHead,movecoords._newHead);
				eatMove(movecoords,f);
			}
			this._matrix.print();
		}
		else{
			GameManager.print("Game Over");
			this._GameManager.gameOver(this.get_score());
		}
	}
	/**
	 * called when snake enters a food cell
	 * @param movecoordinates
	 * @param f type of food to eat
	 */
	private void eatMove(MakeMoveParameter movecoordinates,Food f) {
		GameManager.print("eating");
		this.eat(f);
		this._GameManager.addFood();
	}
	/**
	 * called when snake enters an empty cell
	 * @param movecoordinates
	 */
	private void noEatMove(MakeMoveParameter movecoordinates) {
		GameManager.print("not eating");
		GameManager.print("no food, advancing tail");
		this._matrix.advanceTail(movecoordinates._oldTail);
		this._body.removeLast();
	}
	/**
	 * advances the head by one cell according to new coordinates
	 * @param oldHead old head coordinates
	 * @param newHead new head coordinates
	 */
	private void advanceHeadOnce(Coordinate oldHead, Coordinate newHead) {
		GameManager.print("advancing head");
		this._matrix.advanceHead(oldHead,newHead);
		this._body.addFirst(newHead);
	}
	/**
	 * this function checks if the new Coordinates into which the snake
	 * wishes to move are legal.
	 * @param newHead new head coordinates
	 * @return 	-1 if cell is a wall or the snake's body
	 * 			1 if cell is empty
	 * 			2 if cell had food in it
	 */
	private int isLegalMove(Coordinate newHead) {
		int newX = newHead.get_x();
		int newY = newHead.get_y();
		if((newX >= this._MatrixSize) || (newX < 0) || 
				(newY >= this._MatrixSize) || (newY < 0)){
			GameManager.print("ran into a wall");
			return -1;
		}
		CellType c = this._matrix.getCell(newX, newY).get_type();
		if (CellType.isSnake(c)){
			GameManager.print("ran into self");
			return -1;
		}
		if (CellType.isFood(c)){
			GameManager.print("food!");
			return 2;
		}
		GameManager.print("normal move");
		return 1;
	}
	/**
	 * this function changes the snake's movement if possible.
	 * @param direction
	 */
	public synchronized void changeDirection(Direction direction){
		if(!Direction.checkOpposite(get_direction(), direction)){
			GameManager.print("changing directions");
			if(this.is_isFuzzy()){
				if(get_direction() != direction){
					GameManager.print("changing from "+_Direction + 
							" to "+direction);
					this.set_direction(Direction.Opposite(direction));
				}
			}else 
				this.set_direction(direction);
		}
	}
	/**
	 * update player's score
	 * @param _score amount to add/subtract
	 */
	protected void updateScore(int _score) {
		this._score += _score;
		this._GameManager.updateScore();
		GameManager.print("Score: "+_score);
	}
	/**
	 * resets player's score to zero
	 */
	protected void resetScore(){
		this._score = 0;
	}
	/* =============Inherited Methods============= */
	/**
	 * call to VisitedFood object's accept method to find out which Food object 
	 * it is
	 * @param f
	 */
	protected void eat(Food f) {
		GameManager.print("in Snake.eat");
		if(f == null){
			System.out.println("null");
		}
		f.accept(this);
	}
	/**
	 * calculates new snake body after eating meat
	 */
	@Override
	public abstract void visit(Meat meat);
	/**
	 * calculates new snake body after eating ice cream
	 */
	@Override
	public abstract void visit(IceCream icecream);
	/**
	 * calculates new snake body after eating insect
	 */
	@Override
	public abstract void visit(Insect insect);
	/* =============Getters & Setters============= */
	/**
	 * setter
	 * @param direction
	 */
	private void set_direction(Direction direction) {
		this._Direction = direction;	
	}
	/**
	 * getter
	 * @return
	 */
	public Direction get_direction() {
		return this._Direction;
	}
	/**
	 * getter
	 * @return
	 */
	public int get_score() {
		return _score;
	}
	/**
	 * setter
	 * @param _type
	 */
	public void set_type(String _type) {
		this._type = _type;
	}
	/**
	 * getter
	 * @return
	 */
	public String get_type() {
		return _type;
	}
	public void changeFuzzy() {
		this._isFuzzy = !_isFuzzy;
	}
	public boolean is_isFuzzy() {
		return _isFuzzy;
	}
	protected void set_GameManager(GameManager _GameManager) {
		this._GameManager = _GameManager;
	}
	protected GameManager get_GameManager() {
		return _GameManager;
	}
	public void repaint() {
		this._GameManager.repaint();
	}
	public Snake getInstance() {
		return this;
	}
}
